/*
 * Copyright 2007 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.sockets.client;

import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;

/**
 * The generic class for TextSocket and BinarySocket. It contains the common things for both
 * {@link com.google.gwt.sockets.client.TextSocket TextSocket} and
 * {@link com.google.gwt.sockets.client.BinarySocket BinarySocket}.
 *
 * @see com.google.gwt.sockets.client.TextSocket
 * @see com.google.gwt.sockets.client.BinarySocket
 * @author Aleksey Lagoshin
 */
public abstract class AbstractSocket {

  private static final String EXCEPTION_SOCKET_NOT_READY = "The implementation of socket is not ready. " +
                                                           "Use 'onReady' event of 'SocketListener'";
  private static final String EXCEPTION_SOCKET_IS_NOT_CONNECTED = "Socket is not connected";
  private static final String EXCEPTION_SOCKET_WAS_NOT_CONNECTED = "Socket has never been connected";
  private static final String EXCEPTION_SOCKET_IS_SHUTTED_DOWN = "Socket is shutted down";

  /**
   * The ID for next instance of socket classes, is needed if there are several sockets in one application.
   */
  private static int nextId = 1;

  /**
   * The JavaScript object which should provide common socket functions (connect, disconnect, all read/write
   * functions, etc). It works as proxy object.
   */
  protected JavaScriptObject socketObj = null;

  /**
   * The ID of instance.
   */
  private int id;

  /**
   * The state of connection.
   */
  protected boolean connected = false;

  /**
   * False if this socket has never been connected to server.
   */
  private boolean wasConnected = false;

  /**
   * True if socket has been shutted down.
   */
  private boolean shuttedDown = false;

  private SocketListenerCollection listeners;

  /**
   * Default constructor.
   */
  protected AbstractSocket() {
  }

  /**
   * Standart constructor for socket classes.
   *
   * @param listener a listener for the socket
   */
  public AbstractSocket(SocketListener listener) {
    initialize(listener);
  }

  protected void initialize(SocketListener listener) {
    id = nextId++;

    addSocketListener(listener);

    initFunctions(id);
    initImplementation(id);
  }

  /**
   * Initializes an implementation of this socket.
   *
   * @param id the ID of this socket
   */
  protected abstract void initImplementation(int id);

  /**
   * Adds a listener to receive events from this socket.
   *
   * @param listener a listener
   */
  public void addSocketListener(SocketListener listener) {
    if (listeners == null)
      listeners = new SocketListenerCollection();
    listeners.add(listener);
  }

  /**
   * Removes a listener from this socket.
   *
   * @param listener a listener
   */
  public void removeSocketListener(SocketListener listener) {
    if (listeners != null)
      listeners.remove(listener);
  }

  /**
   * Tries to connect to specified host (could be IP or domain name). If connection is successful,
   * then <code>{@link SocketListener#onConnect() onConnect}</code> method of <code>SocketListener</code>
   * will be executed, otherwise <code>{@link SocketListener#onIOError(String) onIOError}</code> or
   * <code>{@link SocketListener#onSecurityError(String) onSecurityError}</code> method will be called.
   *
   * @param host the host, if null then socket will try to connect to server where this GWT application
   * is located.
   * @param port the port
   * @throws SocketException raised if socket is not ready
   */
  public void connect(String host, int port) throws SocketException {
    checkSocket();
    connected = false;
    connectImpl(host != null ? host : getHomeHost(), port);
  }

  /**
   * Closes active connection.
   *
   * @throws IOException raised if an I/O error occurs
   */
  public void disconnect() throws IOException {
    checkSocketAndConnection(false);
    try {
      connected = false;
      disconnectImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /**
   * Does all needed things to remove socket object.
   *
   * @throws SocketException raised if socket is not ready
   */
  public void shutdown() throws SocketException {
    socketObj = null;
    shuttedDown = true;
  }

  /**
   * Returns the state of the socket's implementation.
   *
   * @return true, if this implementation is loaded and ready to work
   */
  public boolean isReady() {
    return socketObj != null;
  }

  /**
   * Returns true, if socket has been shutted down. Such scokets is no more able to work.
   *
   * @return true, if socket has been shutted down
   */
  public boolean isShuttedDown() {
    return shuttedDown;
  }

  /**
   * Returns the connection state of the socket.
   *
   * @return true if socket is connected to a server
   */
  public boolean isConnected() {
    return connected;
  }

  /**
   * Returns the amount of available bytes which can be read.
   *
   * @return available bytes which can be read
   * @throws IOException raised if an I/O error occurs
   */
  public int available() throws IOException {
    checkSocketAndConnection(true);
    try {
      return availableImpl();
    }
    catch (JavaScriptException e) {
      throw getException(e);
    }
  }

  /*
   Handlers for events from socket object.
  */

  /**
   * Fires a ready event to all listeners.
   *
   * @param socketObj a socket object
   */
  protected void onReady(JavaScriptObject socketObj) {
    this.socketObj = socketObj;
    listeners.fireReady();
  }

  /**
   * Fires a connection event to all listeners.
   */
  protected void onConnect() {
    wasConnected = true;
    connected = true;
    listeners.fireConnect();
  }

  /**
   * Fires a close connection event to all listeners.
   */
  protected void onClose() {
    connected = false;
    listeners.fireCloseConnection();
  }

  /**
   * Fires a data event to all listeners.
   *
   * @param loadedBytes number of received bytes
   */
  protected void onData(int loadedBytes) {
    listeners.fireData(loadedBytes);
  }

  /**
   * Fires an input/output error event to all listeners.
   *
   * @param message detailed message
   */
  protected void onIOError(String message) {
    listeners.fireIOError(message);
  }

  /**
   * Fires a security error event to all listeners.
   *
   * @param message detailed message
   */
  protected void onSecurityError(String message) {
    listeners.fireSecurityError(message);
  }

  /*
   Native implementations of methods.
   Most of executions of socketObj functions are checked by checkResult static method to determine if that
   execution is successful or with exception.
  */

  protected native void connectImpl(String host, int port) /*-{
    this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.connect(host, port);
  }-*/;

  protected native void disconnectImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.close());
    if (ro.ex) throw ro.res;
  }-*/;

  protected native int availableImpl() /*-{
    ro = $wnd._cr(this.@com.google.gwt.sockets.client.AbstractSocket::socketObj.available());
    if (ro.ex) throw ro.res; else return ro.res;
  }-*/;

  /**
   * This method creates JavaScript functions which will be executed by socket implementations.
   * Each function is a dispatcher for events from socket implementation and it executes standart Java
   * method (onReady, onConnect, and other such methods of this class).
   *
   * @param id the ID of the instance
   */
  private native void initFunctions(int id) /*-{
    var prefix = this.@com.google.gwt.sockets.client.AbstractSocket::getPrefix()();
    var instance = this;

    $wnd[prefix + id + "_onReady"] = function(applet) {
      instance.@com.google.gwt.sockets.client.AbstractSocket::onReady(Lcom/google/gwt/core/client/JavaScriptObject;)(applet);
    }
    $wnd[prefix + id + "_onConnect"] = function() {
      instance.@com.google.gwt.sockets.client.AbstractSocket::onConnect()();
    }
    $wnd[prefix + id + "_onClose"] = function() {
      instance.@com.google.gwt.sockets.client.AbstractSocket::onClose()();
    }
    $wnd[prefix + id + "_onData"] = function(loadedBytes) {
      instance.@com.google.gwt.sockets.client.AbstractSocket::onData(I)(loadedBytes);
    }
    $wnd[prefix + id + "_onIOError"] = function(msg) {
      instance.@com.google.gwt.sockets.client.AbstractSocket::onIOError(Ljava/lang/String;)(msg);
    }
    $wnd[prefix + id + "_onSecurityError"] = function(msg) {
      instance.@com.google.gwt.sockets.client.AbstractSocket::onSecurityError(Ljava/lang/String;)(msg);
    }

    // This function checks the return value of socketObj functions and determines whether
    // there is an exception. Actually, this function can be a JSNI function, but due to type
    // converting error in hosted mode I decided to put it here (In hosted mode Shell just can't
    // convert variant type from socketObj functions to JavaScript object, so it is impossible to
    // pass return value to this function as parameter). This works fine in hosted mode.
    if ($wnd._cr == null)
      $wnd._cr = function(result) {
        var resObj = new Object();
        resObj.ex = false;

        if (result == undefined) {
          resObj.res = null;
          return resObj;
        }

        try {
          // Checking if result is a string with exception
          // Java Applet returns "string"(actually, object) without charCodeAt function
          var exCharCode;
          try {
            exCharCode = result.charCodeAt(0);
          }
          catch (e) {
            exCharCode = result.charAt(0)
          }
          // readBytes, readUTF, readString and readStringLine methods return strings, if the first
          // character is 'E' it is an exception
          if (exCharCode != 69) {
            // As Java Applet returns objects instead of strings we need to do type-converting
            resObj.res = "" + result.substring(1);
            return resObj;
          }
        }
        catch (e) {
          // It's not a string with exception, so we can return the value
          resObj.res = result;
          return resObj;
        }

        // Here we know that the result is an exception. Creating a JS exception...
        var ex = new Object();
        ex.name = "" + result.substring(1, 2);
        ex.message = result.substring(2);
        if (ex.message == "null")
          ex.message = null;

        // The second error in hosted mode(IE) is impossibility to throw an exception from here to JSNI functions
        // So we just return that exception, then we need to check whether return value
        // is an object (exception)
        resObj.ex = true;
        resObj.res = ex;
        return resObj;
      }
  }-*/;

  /**
   * Returns a correct Java exception according to specified JavaScriptException.
   *
   * @param e a JavaScript exception
   * @return a correct Java exception, like IOException, EOFException
   */
  protected static IOException getException(JavaScriptException e) {
    // e.getName contains not correct string (actually it's native string from JS), so e.getName().charAt(0) will
    // always throw an exception. "" + e.getName() is a workaround for this issue.
    String name = "" + e.getName();
    // The first character of name field is the type of specified JavaScript exception
    switch (name.charAt(0)) {
      case '1':
        return new IOException(e.getDescription());
      case '2':
        return new EOFException(e.getDescription());
      default:
        return new IOException("Unknown exception");
    }
  }

  /**
   * Checks if this socket is ready to work.
   *
   * @throws SocketException raised if this socket is not ready
   */
  protected void checkSocket() throws SocketException {
    if (socketObj == null)
      throw new SocketException(EXCEPTION_SOCKET_NOT_READY);
  }

  /**
   * Checks if this socket is ready to work and is connected to server.
   *
   * @param checkWasConnected if true, this method will check whether this socket has been connected to server
   * @throws SocketException raised if socket is not ready or if this socket is not connected or if it has
   *                         never been connected (depends on checkWasConnected parameter)
   */
  protected void checkSocketAndConnection(boolean checkWasConnected) throws SocketException {
    if (socketObj == null) {
      if (shuttedDown)
        throw new SocketException(EXCEPTION_SOCKET_IS_SHUTTED_DOWN);
      else
        throw new SocketException(EXCEPTION_SOCKET_NOT_READY);
    }
    if (!checkWasConnected && !connected)
      throw new SocketException(EXCEPTION_SOCKET_IS_NOT_CONNECTED);
    if (checkWasConnected && !wasConnected)
      throw new SocketException(EXCEPTION_SOCKET_WAS_NOT_CONNECTED);
  }

  /**
   * Returns a host of a server where this GWT application is located.
   *
   * @return a host
   */
  protected static native String getHomeHost() /*-{
    return $wnd.location.hostname;
  }-*/;

  /**
   * Returns the ID of instance.
   *
   * @return the ID of instance
   */
  protected int getId() {
    return id;
  }

  /**
   * The prefix for names of JavaScript functions (see initFunctions method), must be unique for each
   * implementation.
   *
   * @return the prefix
   */
  protected abstract String getPrefix();

}
